package io.getquill

import io.getquill.ast._
import io.getquill.context.{CanInsertReturningWithSingleValue, CanInsertWithSingleValue, CanReturnMultiField}
import io.getquill.context.sql._
import io.getquill.context.sql.idiom.SqlIdiom.ActionTableAliasBehavior
import io.getquill.context.sql.idiom._
import io.getquill.idiom.StatementInterpolator._
import io.getquill.idiom.{Statement, StringToken, Token}
import io.getquill.norm.ConcatBehavior.NonAnsiConcat
import io.getquill.norm.ConcatBehavior
import io.getquill.sql.idiom.BooleanLiteralSupport

trait OracleDialect
    extends SqlIdiom
    with QuestionMarkBindVariables
    with ConcatSupport
    with CanReturnMultiField
    with BooleanLiteralSupport
    with CanInsertWithSingleValue
    with CanInsertReturningWithSingleValue {

  override def useActionTableAliasAs: ActionTableAliasBehavior = ActionTableAliasBehavior.Hide

  class OracleFlattenSqlQueryTokenizerHelper(q: FlattenSqlQuery)(implicit
    astTokenizer: Tokenizer[Ast],
    strategy: NamingStrategy,
    idiomContext: IdiomContext
  ) extends FlattenSqlQueryTokenizerHelper(q)(astTokenizer, strategy, idiomContext) {
    import q._

    override def withFrom: Statement = from match {
      case Nil =>
        stmt"$withDistinct FROM DUAL"
      case _ =>
        super.withFrom
    }
  }

  override implicit def sqlQueryTokenizer(implicit
    astTokenizer: Tokenizer[Ast],
    strategy: NamingStrategy,
    idiomContext: IdiomContext
  ): Tokenizer[SqlQuery] = Tokenizer[SqlQuery] {
    case q: FlattenSqlQuery =>
      new OracleFlattenSqlQueryTokenizerHelper(q).apply
    case other =>
      super.sqlQueryTokenizer.token(other)
  }

  override def concatBehavior: ConcatBehavior = NonAnsiConcat

  private val _emptySetContainsToken: StringToken = StringToken("1 <> 1")

  override def emptySetContainsToken(field: Token): Token = _emptySetContainsToken

  override protected def limitOffsetToken(
    query: Statement
  )(implicit astTokenizer: Tokenizer[Ast], strategy: NamingStrategy): Tokenizer[(Option[Ast], Option[Ast])] =
    Tokenizer[(Option[Ast], Option[Ast])] {
      case (Some(limit), None)         => stmt"$query FETCH FIRST ${limit.token} ROWS ONLY"
      case (Some(limit), Some(offset)) => stmt"$query OFFSET ${offset.token} ROWS FETCH NEXT ${limit.token} ROWS ONLY"
      case (None, Some(offset))        => stmt"$query OFFSET ${offset.token} ROWS"
      case other                       => super.limitOffsetToken(query).token(other)
    }

  override implicit def operationTokenizer(implicit
    astTokenizer: Tokenizer[Ast],
    strategy: NamingStrategy
  ): Tokenizer[Operation] =
    Tokenizer[Operation] {
      case BinaryOperation(a, NumericOperator.`%`, b) => stmt"MOD(${a.token}, ${b.token})"
      case other                                      => super.operationTokenizer.token(other)
    }

  override protected def tokenizeColumn(strategy: NamingStrategy, column: String, renameable: Renameable): String =
    tokenizeEscapeUnderscores(strategy, column, Some(renameable))

  override protected def tokenizeTable(strategy: NamingStrategy, table: String, renameable: Renameable): String =
    tokenizeEscapeUnderscores(strategy, table, Some(renameable))

  override protected def tokenizeColumnAlias(strategy: NamingStrategy, column: String): String =
    tokenizeEscapeUnderscores(strategy, column, None)

  override protected def tokenizeTableAlias(strategy: NamingStrategy, column: String): String =
    tokenizeEscapeUnderscores(strategy, column, None)

  private def tokenizeEscapeUnderscores(
    strategy: NamingStrategy,
    columnOrTable: String,
    renameable: Option[Renameable]
  ): String =
    if (columnOrTable.startsWith("_"))
      Escape.column(columnOrTable)
    else
      renameable match {
        case Some(renameable) => renameable.fixedOr(columnOrTable)(strategy.column(columnOrTable))
        case None             => strategy.column(columnOrTable)
      }

  override def defaultAutoGeneratedToken(field: Token) = stmt"($field) VALUES (DEFAULT)"

  override def prepareForProbing(string: String): String = string
}

object OracleDialect extends OracleDialect
